home *** CD-ROM | disk | FTP | other *** search
/ Enter 2006 September / Enter 09 2006.iso / Internet / SpamExperts Home 1.1 / SpamExperts Home.exe / lib / spamexperts.modules / spambayes / UserInterface.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2006-07-14  |  38.9 KB  |  1,081 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.4)
  3.  
  4. '''Web User Interface
  5.  
  6. Classes:
  7.     UserInterfaceServer - Implements the web server component
  8.                           via a Dibbler plugin.
  9.     BaseUserInterface - Just has utilities for creating boxes and so forth.
  10.                         (Does not include any pages)
  11.     UserInterface - A base class for Spambayes web user interfaces.
  12.  
  13. Abstract:
  14.  
  15. This module implements a browser based Spambayes user interface.  Users can
  16. *not* use this class (there is no \'home\' page), but developments should
  17. sub-class it to provide an appropriate interface for their application.
  18.  
  19. Functions deemed appropriate for all application interfaces are included.
  20. These currently include:
  21.   onClassify - classify a given message
  22.   onWordquery - query a word from the database
  23.   onTrain - train a message or mbox
  24.   onSave - save the database and possibly shutdown
  25.   onConfig - present the appropriate configuration page
  26.   onAdvancedconfig - present the appropriate advanced configuration page
  27.   onExperimentalconfig - present the experimental options configuration page
  28.   onHelp - present the help page
  29.   onStats - present statistics information
  30.   onBugreport - help the user fill out a bug report
  31.  
  32. To Do:
  33.  
  34. Web training interface:
  35.  
  36.  o Functional tests.
  37.  o Keyboard navigation (David Ascher).  But aren\'t Tab and left/right
  38.    arrow enough?
  39.  
  40.  
  41. User interface improvements:
  42.  
  43.  o Once the pieces are on separate pages, make the paste box bigger.
  44.  o Deployment: Windows executable?  atlaxwin and ctypes?  Or just
  45.    webbrowser?
  46.  o Save the stats (num classified, etc.) between sessions.
  47.  o "Reload database" button.
  48.  o Displaying options should be done with the locale format function
  49.    rather than str().
  50.  o Suggestions?
  51.  
  52. '''
  53. __author__ = 'Richie Hindle <richie@entrian.com>,\n                Tim Stone <tim@fourstonesExpressions.com>'
  54. __credits__ = 'Tim Peters, Neale Pickett, Tony Meyer, all the Spambayes folk.'
  55.  
  56. try:
  57.     (True, False)
  58. except NameError:
  59.     (True, False) = (1, 0)
  60.  
  61. import re
  62. import os
  63. import sys
  64. import time
  65. import email
  66. import smtplib
  67. import binascii
  68. import cgi
  69. import mailbox
  70. import types
  71. import StringIO
  72. import oe_mailbox
  73. import PyMeldLite
  74. import Dibbler
  75. import tokenizer
  76. from spambayes import Stats
  77. from spambayes import Version
  78. from spambayes import storage
  79. from spambayes import FileCorpus
  80. from Options import options, optionsPathname, defaults, OptionsClass, _
  81. IMAGES = ('helmet', 'status', 'config', 'help', 'message', 'train', 'classify', 'query')
  82. experimental_ini_map = (('Experimental Options', None),)
  83. for opt in options.options(True):
  84.     (sect, opt) = opt[1:].split(']', 1)
  85.     if opt[:2].lower() == 'x-' and not options.doc(sect, opt).lower().startswith(_('(deprecated)')):
  86.         experimental_ini_map += ((sect, opt),)
  87.         continue
  88.  
  89.  
  90. class UserInterfaceServer(Dibbler.HTTPServer):
  91.     '''Implements the web server component via a Dibbler plugin.'''
  92.     
  93.     def __init__(self, uiPort):
  94.         Dibbler.HTTPServer.__init__(self, uiPort)
  95.         print _('User interface url is http://localhost:%d/') % uiPort
  96.  
  97.     
  98.     def requestAuthenticationMode(self):
  99.         return options[('html_ui', 'http_authentication')]
  100.  
  101.     
  102.     def getRealm(self):
  103.         return _('SpamBayes Web Interface')
  104.  
  105.     
  106.     def isValidUser(self, name, password):
  107.         if name == options[('html_ui', 'http_user_name')]:
  108.             pass
  109.         return password == options[('html_ui', 'http_password')]
  110.  
  111.     
  112.     def getPasswordForUser(self, name):
  113.         return options[('html_ui', 'http_password')]
  114.  
  115.     
  116.     def getCancelMessage(self):
  117.         return _('You must login to use SpamBayes.')
  118.  
  119.  
  120.  
  121. class BaseUserInterface(Dibbler.HTTPPlugin):
  122.     
  123.     def __init__(self, lang_manager = None):
  124.         Dibbler.HTTPPlugin.__init__(self)
  125.         self.lang_manager = lang_manager
  126.         (htmlSource, self._images) = self.readUIResources()
  127.         self.html = PyMeldLite.Meld(htmlSource, readonly = True)
  128.         self.app_for_version = 'SpamBayes'
  129.  
  130.     
  131.     def onIncomingConnection(self, clientSocket):
  132.         '''Checks the security settings.'''
  133.         remoteIP = clientSocket.getpeername()[0]
  134.         trustedIPs = options[('html_ui', 'allow_remote_connections')]
  135.         if trustedIPs == '*' or remoteIP == clientSocket.getsockname()[0]:
  136.             return True
  137.         
  138.         trustedIPs = trustedIPs.replace('.', '\\.').replace('*', '([01]?\\d\\d?|2[04]\\d|25[0-5])')
  139.         for trusted in trustedIPs.split(','):
  140.             if re.search('^' + trusted + '$', remoteIP):
  141.                 return True
  142.                 continue
  143.         
  144.         return False
  145.  
  146.     
  147.     def _getHTMLClone(self, help_topic = None):
  148.         '''Gets a clone of the HTML, with the footer timestamped, and
  149.         version information added, ready to be modified and sent to the
  150.         browser.'''
  151.         clone = self.html.clone()
  152.         timestamp = time.strftime('%H:%M on %A %B %d %Y', time.localtime())
  153.         clone.footer.timestamp = timestamp
  154.         v = Version.get_current_version()
  155.         clone.footer.version = v.get_long_version(self.app_for_version)
  156.         if help_topic:
  157.             clone.helplink.href = 'help?topic=%s' % (help_topic,)
  158.         
  159.         return clone
  160.  
  161.     
  162.     def _writePreamble(self, name, parent = None, showImage = True):
  163.         """Writes the HTML for the beginning of a page - time-consuming
  164.         methlets use this and `_writePostamble` to write the page in
  165.         pieces, including progress messages.  `parent` (if given) should
  166.         be a pair: `(url, label)`, eg. `('review', 'Review')`."""
  167.         html = self._getHTMLClone()
  168.         html.mainContent = ' '
  169.         del html.footer
  170.         html.title = name
  171.         if name == _('Home'):
  172.             del html.homelink
  173.             html.pagename = _('Home')
  174.         elif parent:
  175.             html.pagename = "> <a href='%s'>%s</a> > %s" % (parent[0], parent[1], name)
  176.         else:
  177.             html.pagename = '> ' + name
  178.         if not showImage:
  179.             del html.helmet
  180.         
  181.         self.writeOKHeaders('text/html')
  182.         self.write(re.sub('</div>\\s*</body>\\s*</html>', '', str(html)))
  183.  
  184.     
  185.     def _writePostamble(self, help_topic = None):
  186.         '''Writes the end of time-consuming pages - see `_writePreamble`.'''
  187.         self.write('</div>' + self._getHTMLClone(help_topic).footer)
  188.         self.write('</body></html>')
  189.  
  190.     
  191.     def _trimHeader(self, field, limit, quote = False):
  192.         """Trims a string, adding an ellipsis if necessary and HTML-quoting
  193.         on request.  Also pumps it through email.Header.decode_header, which
  194.         understands charset sections in email headers - I suspect this will
  195.         only work for Latin character sets, but hey, it works for Francois
  196.         Granger's name.  8-)"""
  197.         
  198.         try:
  199.             sections = email.Header.decode_header(field)
  200.         except (binascii.Error, email.Errors.HeaderParseError):
  201.             sections = [
  202.                 (field, None)]
  203.  
  204.         field = []([ text for text, unused in sections ])
  205.         if quote:
  206.             field = cgi.escape(field)
  207.         
  208.         return field
  209.  
  210.     
  211.     def onHome(self):
  212.         '''Serve up the homepage.'''
  213.         raise NotImplementedError
  214.  
  215.     
  216.     def _writeImage(self, image):
  217.         self.writeOKHeaders('image/gif')
  218.         self.write(self._images[image])
  219.  
  220.     for imageName in IMAGES:
  221.         exec "def %s(self): self._writeImage('%s')" % ('on%sGif' % imageName.capitalize(), imageName)
  222.     
  223.     
  224.     def _buildBox(self, heading, icon, content):
  225.         '''Builds a yellow-headed HTML box.'''
  226.         box = self.html.headedBox.clone()
  227.         box.heading = heading
  228.         if icon:
  229.             box.icon.src = icon
  230.         else:
  231.             del box.iconCell
  232.         box.boxContent = content
  233.         return box
  234.  
  235.     
  236.     def readUIResources(self):
  237.         '''Returns ui.html and a dictionary of Gifs.'''
  238.         if self.lang_manager:
  239.             ui_html = self.lang_manager.import_ui_html()
  240.         else:
  241.             ui_html = ui_html
  242.             import spambayes.resources
  243.         images = { }
  244.         for baseName in IMAGES:
  245.             moduleName = '%s.%s_gif' % ('spambayes.resources', baseName)
  246.             module = __import__(moduleName, { }, { }, ('spambayes', 'resources'))
  247.             images[baseName] = module.data
  248.         
  249.         return (ui_html.data, images)
  250.  
  251.  
  252.  
  253. class UserInterface(BaseUserInterface):
  254.     '''Serves the HTML user interface.'''
  255.     
  256.     def __init__(self, bayes, config_parms = (), adv_parms = (), lang_manager = None, stats = None):
  257.         '''Load up the necessary resources: ui.html and helmet.gif.'''
  258.         BaseUserInterface.__init__(self, lang_manager)
  259.         self.classifier = bayes
  260.         self.parm_ini_map = config_parms
  261.         self.advanced_options_map = adv_parms
  262.         self.stats = stats
  263.         self.app_for_version = None
  264.  
  265.     
  266.     def onClassify(self, file, text, which):
  267.         '''Classify an uploaded or pasted message.'''
  268.         self._writePreamble(_('Classify'))
  269.         if not file:
  270.             pass
  271.         message = text
  272.         message = message.replace('\r\n', '\n').replace('\r', '\n')
  273.         results = self._buildCluesTable(message)
  274.         results.classifyAnother = self._buildClassifyBox()
  275.         self.write(results)
  276.         self._writePostamble()
  277.  
  278.     ev_re = re.compile('%s:(.*?)(?:\n\\S|\n\n)' % re.escape(options[('Headers', 'evidence_header_name')]), re.DOTALL)
  279.     sc_re = re.compile('%s:\\s*([\\d.]+)' % re.escape(options[('Headers', 'score_header_name')]))
  280.     
  281.     def _fillCluesTable(self, clues):
  282.         accuracy = 6
  283.         cluesTable = self.html.cluesTable.clone()
  284.         cluesRow = cluesTable.cluesRow.clone()
  285.         del cluesTable.cluesRow
  286.         fetchword = self.classifier._wordinfoget
  287.         for word, wordProb in clues:
  288.             record = fetchword(word)
  289.             if record:
  290.                 nham = record.hamcount
  291.                 nspam = record.spamcount
  292.                 if wordProb is None:
  293.                     wordProb = self.classifier.probability(record)
  294.                 
  295.             elif word != '*H*' and word != '*S*':
  296.                 nham = nspam = 0
  297.             else:
  298.                 nham = nspam = '-'
  299.             if wordProb is None:
  300.                 wordProb = '-'
  301.             else:
  302.                 wordProb = round(float(wordProb), accuracy)
  303.             cluesTable += cluesRow % (cgi.escape(word), wordProb, nham, nspam)
  304.         
  305.         return cluesTable
  306.  
  307.     
  308.     def _buildCluesTable(self, message, subject = None, show_tokens = False):
  309.         tokens = list(tokenizer.tokenize(message))
  310.         results = self.html.classifyResults.clone()
  311.         results.probability = '%.2f%% (%s)' % (probability * 100, probability)
  312.         if subject is None:
  313.             heading = '%s: (%s)' % (head_name, len(clues))
  314.         else:
  315.             heading = '%s for: %s (%s)' % (head_name, subject, len(clues))
  316.         results.cluesBox = self._buildBox(heading, 'status.gif', cluesTable)
  317.         if not show_tokens:
  318.             mo = self.sc_re.search(message)
  319.             if mo:
  320.                 prob = float(mo.group(1).strip())
  321.                 results.orig_prob_num = '%.2f%% (%s)' % (prob * 100, prob)
  322.             else:
  323.                 del results.orig_prob
  324.             mo = self.ev_re.search(message)
  325.             if mo:
  326.                 clues = []
  327.                 evidence = re.findall("'(.+?)': ([^;]+)(?:;|$)", mo.group(1))
  328.                 for word, prob in evidence:
  329.                     clues.append((word, prob))
  330.                 
  331.                 cluesTable = self._fillCluesTable(clues)
  332.                 if subject is None:
  333.                     heading = _('Original clues: (%s)') % (len(evidence),)
  334.                 else:
  335.                     heading = _('Original clues for: %s (%s)') % (subject, len(evidence))
  336.                 orig_results = self._buildBox(heading, 'status.gif', cluesTable)
  337.                 results.cluesBox += orig_results
  338.             
  339.         else:
  340.             del results.orig_prob
  341.         return results
  342.  
  343.     
  344.     def onWordquery(self, word, query_type = _('basic'), max_results = '10', ignore_case = False):
  345.         
  346.         try:
  347.             max_results = int(max_results)
  348.         except ValueError:
  349.             max_results = 10
  350.  
  351.         original_word = word
  352.         query = self.html.wordQuery.clone()
  353.         query.word.value = '%s' % (word,)
  354.         for q_type in [
  355.             query.advanced.basic,
  356.             query.advanced.wildcard,
  357.             query.advanced.regex]:
  358.             if query_type == q_type.id:
  359.                 q_type.checked = 'checked'
  360.                 if query_type != _('basic'):
  361.                     del query.advanced.max_results.disabled
  362.                 
  363.             query_type != _('basic')
  364.         
  365.         if ignore_case:
  366.             query.advanced.ignore_case.checked = 'checked'
  367.         
  368.         query.advanced.max_results.value = str(max_results)
  369.         queryBox = self._buildBox(_('Word query'), 'query.gif', query)
  370.         if not options[('html_ui', 'display_adv_find')]:
  371.             del queryBox.advanced
  372.         
  373.         stats = []
  374.         if word == '':
  375.             stats.append(_('You must enter a word.'))
  376.         elif query_type == _('basic') and not ignore_case:
  377.             wordinfo = self.classifier._wordinfoget(word)
  378.             if wordinfo:
  379.                 stat = (word, wordinfo.spamcount, wordinfo.hamcount, self.classifier.probability(wordinfo))
  380.             else:
  381.                 stat = _('%r does not exist in the database.') % cgi.escape(word)
  382.             stats.append(stat)
  383.         elif query_type != _('regex'):
  384.             word = re.escape(word)
  385.         
  386.         if query_type == _('wildcard'):
  387.             word = word.replace('\\?', '.')
  388.             word = word.replace('\\*', '.*')
  389.         
  390.         flags = 0
  391.         if ignore_case:
  392.             flags = re.IGNORECASE
  393.         
  394.         r = re.compile(word, flags)
  395.         reached_limit = False
  396.         for w in self.classifier._wordinfokeys():
  397.             if not reached_limit and len(stats) >= max_results:
  398.                 reached_limit = True
  399.                 over_limit = 0
  400.             
  401.             if r.match(w):
  402.                 if reached_limit:
  403.                     over_limit += 1
  404.                 else:
  405.                     wordinfo = self.classifier._wordinfoget(w)
  406.                     stat = (w, wordinfo.spamcount, wordinfo.hamcount, self.classifier.probability(wordinfo))
  407.                     stats.append(stat)
  408.             reached_limit
  409.         
  410.         if len(stats) == 0 and max_results > 0:
  411.             stat = _("There are no words that begin with '%s' in the database.") % (word,)
  412.             stats.append(stat)
  413.         elif reached_limit:
  414.             stat = _('Additional tokens not shown: %d') % (over_limit,)
  415.             stats.append(stat)
  416.         
  417.         self._writePreamble(_('Word query'))
  418.         self.write(queryBox)
  419.         self._writePostamble()
  420.  
  421.     
  422.     def onTrain(self, file, text, which):
  423.         '''Train on an uploaded or pasted message.'''
  424.         self._writePreamble(_('Train'))
  425.         if not file:
  426.             pass
  427.         content = text
  428.         isSpam = which == _('Train as Spam')
  429.         if file:
  430.             content = self._convertToMbox(content)
  431.         
  432.         content = content.replace('\r\n', '\n').replace('\r', '\n')
  433.         messages = self._convertUploadToMessageList(content)
  434.         if isSpam:
  435.             desired_corpus = 'spamCorpus'
  436.         else:
  437.             desired_corpus = 'hamCorpus'
  438.         if hasattr(self, desired_corpus):
  439.             corpus = getattr(self, desired_corpus)
  440.         elif hasattr(self, 'state'):
  441.             corpus = getattr(self.state, desired_corpus)
  442.             setattr(self, desired_corpus, corpus)
  443.             self.msg_name_func = self.state.getNewMessageName
  444.         elif isSpam:
  445.             fn = storage.get_pathname_option('Storage', 'spam_cache')
  446.         else:
  447.             fn = storage.get_pathname_option('Storage', 'ham_cache')
  448.         storage.ensureDir(fn)
  449.         if options[('Storage', 'cache_use_gzip')]:
  450.             factory = FileCorpus.GzipFileMessageFactory()
  451.         else:
  452.             factory = FileCorpus.FileMessageFactory()
  453.         age = options[('Storage', 'cache_expiry_days')] * 24 * 60 * 60
  454.         corpus = FileCorpus.ExpiryFileCorpus(age, factory, fn, '[0123456789\\-]*', cacheSize = 20)
  455.         setattr(self, desired_corpus, corpus)
  456.         
  457.         class UniqueNamer(object):
  458.             count = -1
  459.             
  460.             def generate_name(self):
  461.                 self.count += 1
  462.                 return '%10.10d-%d' % (long(time.time()), self.count)
  463.  
  464.  
  465.         Namer = UniqueNamer()
  466.         self.msg_name_func = Namer.generate_name
  467.         self.write('<b>' + _('Training') + '...</b>\n')
  468.         self.flush()
  469.         for message in messages:
  470.             key = self.msg_name_func()
  471.             msg = corpus.makeMessage(key, message)
  472.             msg.setId(key)
  473.             corpus.addMessage(msg)
  474.             msg.RememberTrained(isSpam)
  475.             self.stats.RecordTraining(not isSpam)
  476.         
  477.         self._doSave()
  478.         self.write(_('%sOK. Return %sHome%s or train again:%s') % ('<p>', "<a href='home'>", '</a', '</p>'))
  479.         self.write(self._buildTrainBox())
  480.         self._writePostamble()
  481.  
  482.     
  483.     def _convertToMbox(self, content):
  484.         """Check if the given buffer is in a non-mbox format, and convert it
  485.         into mbox format if so.  If it's already an mbox, return it unchanged.
  486.  
  487.         Currently, the only supported non-mbox format is Outlook Express DBX.
  488.         In such a case we use the module oe_mailbox to convert the DBX
  489.         content into a standard mbox file.  Testing if the file is a
  490.         DBX one is very quick (just a matter of checking the first few
  491.         bytes), and should not alter the overall performance."""
  492.         content = oe_mailbox.convertToMbox(content)
  493.         return content
  494.  
  495.     
  496.     def _convertUploadToMessageList(self, content):
  497.         '''Returns a list of raw messages extracted from uploaded content.
  498.         You can upload either a single message or an mbox file.'''
  499.         if content.startswith('From '):
  500.             
  501.             class SimpleMessage:
  502.                 
  503.                 def __init__(self, fp):
  504.                     self.guts = fp.read()
  505.  
  506.  
  507.             contentFile = StringIO.StringIO(content)
  508.             mbox = mailbox.PortableUnixMailbox(contentFile, SimpleMessage)
  509.             return map((lambda m: m.guts), mbox)
  510.         else:
  511.             return [
  512.                 content]
  513.  
  514.     
  515.     def _doSave(self):
  516.         '''Saves the database.'''
  517.         self.write('<b>' + _('Saving...'))
  518.         self.flush()
  519.         self.classifier.store()
  520.         self.write(_('Done.') + '</b>\n')
  521.  
  522.     
  523.     def onSave(self, how):
  524.         '''Command handler for "Save" and "Save & shutdown".'''
  525.         isShutdown = how.lower().find('shutdown') >= 0
  526.         self._writePreamble(_('Save'), showImage = not isShutdown)
  527.         self._doSave()
  528.         if isShutdown:
  529.             self.write('<p>%s</p>' % self.html.shutdownMessage)
  530.             self.write('</div></body></html>')
  531.             self.flush()
  532.             self.close()
  533.             raise SystemExit
  534.         
  535.         self._writePostamble()
  536.  
  537.     
  538.     def _buildClassifyBox(self):
  539.         '''Returns a "Classify a message" box.  This is used on both the Home
  540.         page and the classify results page.  The Classify form is based on the
  541.         Upload form.'''
  542.         form = self.html.upload.clone()
  543.         del form.or_mbox
  544.         del form.submit_spam
  545.         del form.submit_ham
  546.         form.action = 'classify'
  547.         return self._buildBox(_('Classify a message'), 'classify.gif', form)
  548.  
  549.     
  550.     def _buildTrainBox(self):
  551.         '''Returns a "Train on a given message" box.  This is used on both
  552.         the Home page and the training results page.  The Train form is
  553.         based on the Upload form.'''
  554.         form = self.html.upload.clone()
  555.         del form.submit_classify
  556.         return self._buildBox(_('Train on a message, mbox file or dbx file'), 'message.gif', form)
  557.  
  558.     
  559.     def reReadOptions(self):
  560.         '''Called by the config page when the user saves some new options,
  561.         or restores the defaults.'''
  562.         pass
  563.  
  564.     
  565.     def onExperimentalconfig(self):
  566.         html = self._buildConfigPage(experimental_ini_map)
  567.         html.title = _('Home > Experimental Configuration')
  568.         html.pagename = _('> Experimental Configuration')
  569.         html.adv_button.name.value = _('Back to basic configuration')
  570.         html.adv_button.action = 'config'
  571.         html.config_submit.value = _('Save experimental options')
  572.         html.restore.value = _('Restore experimental options defaults (all off)')
  573.         del html.exp_button
  574.         self.writeOKHeaders('text/html')
  575.         self.write(html)
  576.  
  577.     
  578.     def onAdvancedconfig(self):
  579.         html = self._buildConfigPage(self.advanced_options_map)
  580.         html.title = _('Home > Advanced Configuration')
  581.         html.pagename = _('> Advanced Configuration')
  582.         html.adv_button.name.value = _('Back to basic configuration')
  583.         html.adv_button.action = 'config'
  584.         html.config_submit.value = _('Save advanced options')
  585.         html.restore.value = _('Restore advanced options defaults')
  586.         del html.exp_button
  587.         self.writeOKHeaders('text/html')
  588.         self.write(html)
  589.  
  590.     
  591.     def onConfig(self):
  592.         html = self._buildConfigPage(self.parm_ini_map)
  593.         html.title = _('Home > Configure')
  594.         html.pagename = _('> Configure')
  595.         self.writeOKHeaders('text/html')
  596.         self.write(html)
  597.  
  598.     
  599.     def _buildConfigPage(self, parm_map):
  600.         html = self._getHTMLClone()
  601.         html.shutdownTableCell = ' '
  602.         html.mainContent = self.html.configForm.clone()
  603.         html.mainContent.configFormContent = ''
  604.         html.mainContent.optionsPathname = cgi.escape(optionsPathname)
  605.         return self._buildConfigPageBody(html, parm_map)
  606.  
  607.     
  608.     def _buildConfigPageBody(self, html, parm_map):
  609.         configTable = None
  610.         section = None
  611.         for sect, opt in parm_map:
  612.             if opt is None:
  613.                 if configTable is not None and section is not None:
  614.                     section.boxContent = configTable
  615.                     html.configFormContent += section
  616.                 
  617.                 section = self.html.headedBox.clone()
  618.                 configTable = self.html.configTable.clone()
  619.                 configTextRow1 = configTable.configTextRow1.clone()
  620.                 configTextRow2 = configTable.configTextRow2.clone()
  621.                 configCbRow1 = configTable.configCbRow1.clone()
  622.                 configRow2 = configTable.configRow2.clone()
  623.                 blankRow = configTable.blankRow.clone()
  624.                 del configTable.configTextRow1
  625.                 del configTable.configTextRow2
  626.                 del configTable.configCbRow1
  627.                 del configTable.configRow2
  628.                 del configTable.blankRow
  629.                 del configTable.folderRow
  630.                 section.heading = sect
  631.                 del section.iconCell
  632.                 continue
  633.             
  634.             html_key = sect + '_' + opt
  635.             if sect == 'Headers' and opt in ('notate_to', 'notate_subject'):
  636.                 valid_input = (options[('Headers', 'header_ham_string')], options[('Headers', 'header_spam_string')], options[('Headers', 'header_unsure_string')])
  637.             else:
  638.                 valid_input = options.valid_input(sect, opt)
  639.             newConfigRow1.helpCell = '<strong>' + options.display_name(sect, opt) + ':</strong> ' + cgi.escape(options.doc(sect, opt))
  640.             newConfigRow2 = configRow2.clone()
  641.             currentValue = options[(sect, opt)]
  642.             if type(currentValue) in types.StringTypes:
  643.                 currentValue = currentValue.replace(',', ', ')
  644.                 newConfigRow2 = configTextRow2.clone()
  645.             else:
  646.                 currentValue = options.unconvert(sect, opt)
  647.                 newConfigRow2 = configRow2.clone()
  648.             if options.is_boolean(sect, opt):
  649.                 if currentValue == 'False':
  650.                     currentValue = _('No')
  651.                 elif currentValue == 'True':
  652.                     currentValue = _('Yes')
  653.                 
  654.             
  655.             newConfigRow2.currentValue = currentValue
  656.             configTable += newConfigRow1 + newConfigRow2 + blankRow
  657.         
  658.         if section is not None:
  659.             section.boxContent = configTable
  660.             html.configFormContent += section
  661.         
  662.         return html
  663.  
  664.     
  665.     def onChangeopts(self, **parms):
  666.         pmap = self.parm_ini_map
  667.         if parms.has_key('how'):
  668.             if parms['how'] == _('Save advanced options'):
  669.                 pmap = self.advanced_options_map
  670.             elif parms['how'] == _('Save experimental options'):
  671.                 pmap = experimental_ini_map
  672.             
  673.             del parms['how']
  674.         
  675.         html = self._getHTMLClone()
  676.         html.shutdownTableCell = ' '
  677.         html.mainContent = self.html.headedBox.clone()
  678.         errmsg = self.verifyInput(parms, pmap)
  679.         if errmsg != '':
  680.             html.mainContent.heading = _('Errors Detected')
  681.             html.mainContent.boxContent = errmsg
  682.             html.title = _('Home > Error')
  683.             html.pagename = _('> Error')
  684.             self.writeOKHeaders('text/html')
  685.             self.write(html)
  686.             return None
  687.         
  688.         old_database_type = options[('Storage', 'persistent_use_database')]
  689.         old_name = options[('Storage', 'persistent_storage_file')]
  690.         for name, value in parms.items():
  691.             (sect, opt) = name.split('_', 1)
  692.             if (sect, opt) in pmap:
  693.                 options.set(sect, opt, value)
  694.                 continue
  695.             (sect2, opt) = opt.split('_', 1)
  696.             sect += '_' + sect2
  697.             options.set(sect, opt, value)
  698.         
  699.         options.update_file(optionsPathname)
  700.         if options[('Storage', 'persistent_use_database')] != old_database_type and os.path.exists(old_name):
  701.             new_name = options[('Storage', 'persistent_storage_file')]
  702.             new_type = options[('Storage', 'persistent_use_database')]
  703.             self.close_database()
  704.             
  705.             try:
  706.                 os.remove(new_name + '.tmp')
  707.             except OSError:
  708.                 pass
  709.  
  710.             storage.convert(old_name, old_database_type, new_name + '.tmp', new_type)
  711.             if os.path.exists(new_name):
  712.                 
  713.                 try:
  714.                     os.remove(new_name + '.old')
  715.                 except OSError:
  716.                     pass
  717.  
  718.                 os.rename(new_name, new_name + '.old')
  719.             
  720.             os.rename(new_name + '.tmp', new_name)
  721.             if os.path.exists(options[('Storage', 'messageinfo_storage_file')]):
  722.                 
  723.                 try:
  724.                     os.remove(options[('Storage', 'messageinfo_storage_file')] + '.old')
  725.                 except OSError:
  726.                     pass
  727.  
  728.                 os.rename(options[('Storage', 'messageinfo_storage_file')], options[('Storage', 'messageinfo_storage_file')] + '.old')
  729.             
  730.         
  731.         self.reReadOptions()
  732.         html.mainContent.heading = _('Options Changed')
  733.         html.mainContent.boxContent = _("Options changed.  Return <a href='home'>Home</a>.")
  734.         html.title = _('Home > Options Changed')
  735.         html.pagename = _('> Options Changed')
  736.         self.writeOKHeaders('text/html')
  737.         self.write(html)
  738.  
  739.     
  740.     def onRestoredefaults(self, how):
  741.         if how == _('Restore advanced options defaults'):
  742.             self.restoreConfigDefaults(self.advanced_options_map)
  743.         elif how == _('Restore experimental options defaults (all off)'):
  744.             self.restoreConfigDefaults(experimental_ini_map)
  745.         else:
  746.             self.restoreConfigDefaults(self.parm_ini_map)
  747.         self.reReadOptions()
  748.         html = self._getHTMLClone()
  749.         html.shutdownTableCell = ' '
  750.         html.mainContent = self.html.headedBox.clone()
  751.         html.mainContent.heading = _('Option Defaults Restored')
  752.         html.mainContent.boxContent = _("Defaults restored.  Return <a href='home'>Home</a>.")
  753.         html.title = _('Home > Defaults Restored')
  754.         html.pagename = _('> Defaults Restored')
  755.         self.writeOKHeaders('text/html')
  756.         self.write(html)
  757.         self.reReadOptions()
  758.  
  759.     
  760.     def verifyInput(self, parms, pmap):
  761.         '''Check that the given input is valid.'''
  762.         errmsg = ''
  763.         for name, value in parms.items():
  764.             if name[-2:-1] == '-':
  765.                 if parms.has_key(name[:-2]):
  766.                     parms[name[:-2]] += (value,)
  767.                 else:
  768.                     parms[name[:-2]] = (value,)
  769.                 del parms[name]
  770.                 continue
  771.         
  772.         for sect, opt in pmap:
  773.             if opt is None:
  774.                 nice_section_name = sect
  775.                 continue
  776.             
  777.             if sect == 'Headers' and opt in ('notate_to', 'notate_subject'):
  778.                 valid_input = (options[('Headers', 'header_ham_string')], options[('Headers', 'header_spam_string')], options[('Headers', 'header_unsure_string')])
  779.             else:
  780.                 valid_input = options.valid_input(sect, opt)
  781.             html_key = sect + '_' + opt
  782.             if not parms.has_key(html_key):
  783.                 value = ()
  784.                 entered_value = 'None'
  785.             else:
  786.                 value = parms[html_key]
  787.                 entered_value = value
  788.                 if options.is_boolean(sect, opt):
  789.                     if value == _('No'):
  790.                         value = False
  791.                     elif value == _('Yes'):
  792.                         value = True
  793.                     
  794.                 
  795.                 if options.multiple_values_allowed(sect, opt) and value == '':
  796.                     value = ()
  797.                 
  798.                 value = options.convert(sect, opt, value)
  799.             if not options.is_valid(sect, opt, value):
  800.                 errmsg += _("<li>'%s' is not a value valid for [%s] %s") % (entered_value, nice_section_name, options.display_name(sect, opt))
  801.                 if isinstance(valid_input, types.TupleType):
  802.                     errmsg += _('. Valid values are: ')
  803.                     for valid in valid_input:
  804.                         errmsg += str(valid) + ','
  805.                     
  806.                     errmsg = errmsg[:-1]
  807.                 
  808.                 errmsg += '</li>'
  809.             
  810.             parms[html_key] = value
  811.         
  812.         return errmsg
  813.  
  814.     
  815.     def restoreConfigDefaults(self, parm_map):
  816.         d = OptionsClass()
  817.         d.load_defaults(defaults)
  818.         for section, option in parm_map:
  819.             if option is not None:
  820.                 if not options.no_restore(section, option):
  821.                     options.set(section, option, d.get(section, option))
  822.                 
  823.             options.no_restore(section, option)
  824.         
  825.         options.update_file(optionsPathname)
  826.  
  827.     
  828.     def onHelp(self, topic = None):
  829.         '''Provide a help page, either the default if topic is not
  830.         supplied, or specific to the topic given.'''
  831.         self._writePreamble(_('Help'))
  832.         helppage = self.html.helppage.clone()
  833.         if topic:
  834.             headerelem_name = 'helpheader_' + topic
  835.             textelem_name = 'helptext_' + topic
  836.             
  837.             try:
  838.                 helppage.helpheader = self.html[headerelem_name]._content
  839.                 helppage.helptext = self.html[textelem_name]._content % {
  840.                     'cache_expiry_days': options[('Storage', 'cache_expiry_days')] }
  841.             except KeyError:
  842.                 pass
  843.             except:
  844.                 None<EXCEPTION MATCH>KeyError
  845.             
  846.  
  847.         None<EXCEPTION MATCH>KeyError
  848.         self.write(helppage)
  849.         self._writePostamble()
  850.  
  851.     
  852.     def onStats(self):
  853.         '''Provide statistics about previous SpamBayes activity.'''
  854.         self._writePreamble(_('Statistics'))
  855.         if self.stats:
  856.             stats = self.stats.GetStats(use_html = True)
  857.             stats = self._buildBox(_('Statistics'), None, '<br/><br/>'.join(stats))
  858.         else:
  859.             stats = self._buildBox(_('Statistics'), None, _('Statistics not available'))
  860.         self.write(stats)
  861.         self._writePostamble(help_topic = 'stats')
  862.  
  863.     
  864.     def onBugreport(self):
  865.         '''Create a message to post to spambayes@python.org that hopefully
  866.         has enough information for us to help this person with their
  867.         problem.'''
  868.         self._writePreamble(_('Send Help Message'), ('help', _('Help')))
  869.         report = self.html.bugreport.clone()
  870.         v = Version.get_current_version()
  871.         sb_ver = v.get_long_version(self.app_for_version)
  872.         if hasattr(sys, 'frozen'):
  873.             sb_type = 'binary'
  874.         else:
  875.             sb_type = 'source'
  876.         py_ver = sys.version
  877.         
  878.         try:
  879.             os_name = 'Windows %d.%d.%d.%d (%s)' % sys.getwindowsversion()
  880.         except AttributeError:
  881.             os_name = os.name
  882.  
  883.         report.message_body = 'I am using %s (%s), with version %s of Python; my operating system is %s.  I have trained %d ham and %d spam.\n\nThe problem I am having is [DESCRIBE YOUR PROBLEM HERE] ' % (sb_ver, sb_type, py_ver, os_name, self.classifier.nham, self.classifier.nspam)
  884.         remote_servers = options[('pop3proxy', 'remote_servers')]
  885.         if remote_servers:
  886.             domain_guess = remote_servers[0]
  887.             for pre in [
  888.                 'pop.',
  889.                 'pop3.',
  890.                 'mail.']:
  891.                 if domain_guess.startswith(pre):
  892.                     domain_guess = domain_guess[len(pre):]
  893.                     continue
  894.             
  895.         else:
  896.             domain_guess = '[YOUR ISP]'
  897.         report.from_addr.value = '[YOUR EMAIL ADDRESS]@%s' % (domain_guess,)
  898.         report.subject.value = 'Problem with %s: [PROBLEM SUMMARY]' % (self.app_for_version,)
  899.         
  900.         try:
  901.             import win32api
  902.         except ImportError:
  903.             pass
  904.  
  905.         if hasattr(sys, 'frozen'):
  906.             temp_dir = win32api.GetTempPath()
  907.             for name in [
  908.                 'SpamBayesService',
  909.                 'SpamBayesServer']:
  910.                 for i in xrange(3):
  911.                     pn = os.path.join(temp_dir, '%s%d.log' % (name, i + 1))
  912.                     if os.path.exists(pn):
  913.                         report.file.type = 'text'
  914.                         report.file.value = pn
  915.                         break
  916.                         continue
  917.                 
  918.                 if report.file.value:
  919.                     break
  920.                     continue
  921.             
  922.         
  923.         
  924.         try:
  925.             smtp_server = options[('smtpproxy', 'remote_servers')][0]
  926.         except IndexError:
  927.             smtp_server = None
  928.  
  929.         if not smtp_server:
  930.             self.write(self._buildBox(_('Warning'), 'status.gif', _("You will be unable to send this message from this page, as you do not have your SMTP server's details entered in your configuration. Please either <a href='config'>enter those details</a>, or copy the text below into your regular mail application.")))
  931.             del report.submitrow
  932.         
  933.         self.write(report)
  934.         self._writePostamble()
  935.  
  936.     
  937.     def onSubmitreport(self, from_addr, message, subject, attach):
  938.         '''Send the help message/bug report to the specified address.'''
  939.         import mimetypes
  940.         Encoders = Encoders
  941.         import email
  942.         MIMEBase = MIMEBase
  943.         import email.MIMEBase
  944.         MIMEAudio = MIMEAudio
  945.         import email.MIMEAudio
  946.         MIMEMultipart = MIMEMultipart
  947.         import email.MIMEMultipart
  948.         MIMEImage = MIMEImage
  949.         import email.MIMEImage
  950.         MIMEText = MIMEText
  951.         import email.MIMEText
  952.         if not self._verifyEnteredDetails(from_addr, subject, message):
  953.             self._writePreamble(_('Error'), ('help', _('Help')))
  954.             self.write(self._buildBox(_('Error'), 'status.gif', _('You must fill in the details that describe your specific problem before you can send the message.')))
  955.         else:
  956.             self._writePreamble(_('Sent'), ('help', _('Help')))
  957.             mailer = smtplib.SMTP(options[('smtpproxy', 'remote_servers')][0])
  958.             outer = MIMEMultipart()
  959.             outer['Subject'] = subject
  960.             outer['To'] = '"SpamBayes Mailing List" <spambayes@python.org>'
  961.             outer['CC'] = from_addr
  962.             outer['From'] = from_addr
  963.             v = Version.get_current_version()
  964.             outer['X-Mailer'] = v.get_long_version(self.app_for_version)
  965.             outer.preamble = self._wrap(message)
  966.             outer.epilogue = ''
  967.             
  968.             try:
  969.                 (ctype, encoding) = mimetypes.guess_type(attach)
  970.                 if ctype is None or encoding is not None:
  971.                     ctype = 'application/octet-stream'
  972.                 
  973.                 (maintype, subtype) = ctype.split('/', 1)
  974.                 if maintype == 'text':
  975.                     fp = open(attach)
  976.                     msg = MIMEText(fp.read(), _subtype = subtype)
  977.                     fp.close()
  978.                 elif maintype == 'image':
  979.                     fp = open(attach, 'rb')
  980.                     msg = MIMEImage(fp.read(), _subtype = subtype)
  981.                     fp.close()
  982.                 elif maintype == 'audio':
  983.                     fp = open(attach, 'rb')
  984.                     msg = MIMEAudio(fp.read(), _subtype = subtype)
  985.                     fp.close()
  986.                 else:
  987.                     fp = open(attach, 'rb')
  988.                     msg = MIMEBase(maintype, subtype)
  989.                     msg.set_payload(fp.read())
  990.                     fp.close()
  991.                     Encoders.encode_base64(msg)
  992.             except IOError:
  993.                 pass
  994.  
  995.             msg.add_header('Content-Disposition', 'attachment', filename = os.path.basename(attach))
  996.             outer.attach(msg)
  997.             msg = MIMEText(self._wrap(message))
  998.             outer.attach(msg)
  999.             recips = []
  1000.             for r in [
  1001.                 'spambayes@python.org',
  1002.                 from_addr]:
  1003.                 if r:
  1004.                     recips.append(r)
  1005.                     continue
  1006.             
  1007.             mailer.sendmail(from_addr, recips, outer.as_string())
  1008.             self.write(_('Sent message.  Please do not send again, or refresh this page!'))
  1009.         self._writePostamble()
  1010.  
  1011.     
  1012.     def _verifyEnteredDetails(self, from_addr, subject, message):
  1013.         """Ensure that the user didn't just send the form message, and
  1014.         at least changed the fields."""
  1015.         if from_addr.startswith(_('[YOUR EMAIL ADDRESS]')):
  1016.             return False
  1017.         
  1018.         if message.endswith(_('[DESCRIBE YOUR PROBLEM HERE]')):
  1019.             return False
  1020.         
  1021.         if subject.endswith(_('[PROBLEM SUMMARY]')):
  1022.             return False
  1023.         
  1024.         return True
  1025.  
  1026.     
  1027.     def _wrap(self, text, width = 70):
  1028.         '''Wrap the text into lines no bigger than the specified width.'''
  1029.         
  1030.         try:
  1031.             fill = fill
  1032.             import textwrap
  1033.         except ImportError:
  1034.             pass
  1035.  
  1036.         return []([ fill(paragraph, width) for paragraph in text.split('\n') ])
  1037.         
  1038.         def fill(text, width):
  1039.             if len(text) <= width:
  1040.                 return text
  1041.             
  1042.             wordsep_re = re.compile('(-*\\w{2,}-(?=\\w{2,})|(?<=\\S)-{2,}(?=\\w))')
  1043.             chunks = wordsep_re.split(text)
  1044.             chunks = filter(None, chunks)
  1045.             return '\n'.join(self._wrap_chunks(chunks, width))
  1046.  
  1047.         return []([ fill(paragraph, width) for paragraph in text.split('\n') ])
  1048.  
  1049.     
  1050.     def _wrap_chunks(self, chunks, width):
  1051.         '''Stolen from textwrap; see that module in Python >= 2.3 for
  1052.         details.'''
  1053.         lines = []
  1054.         while chunks:
  1055.             cur_line = []
  1056.             cur_len = 0
  1057.             if chunks[0].strip() == '' and lines:
  1058.                 del chunks[0]
  1059.             
  1060.             while chunks:
  1061.                 l = len(chunks[0])
  1062.                 if cur_len + l <= width:
  1063.                     cur_line.append(chunks.pop(0))
  1064.                     cur_len += l
  1065.                     continue
  1066.                 break
  1067.             if chunks and len(chunks[0]) > width:
  1068.                 space_left = width - cur_len
  1069.                 cur_line.append(chunks[0][0:space_left])
  1070.                 chunks[0] = chunks[0][space_left:]
  1071.             
  1072.             if cur_line and cur_line[-1].strip() == '':
  1073.                 del cur_line[-1]
  1074.             
  1075.             if cur_line:
  1076.                 lines.append(''.join(cur_line))
  1077.                 continue
  1078.         return lines
  1079.  
  1080.  
  1081.